Skip to content

gh-98894: Fix dtrace tests in shared builds - #153372

Open
stratakis wants to merge 2 commits into
python:mainfrom
stratakis:shared_probes
Open

gh-98894: Fix dtrace tests in shared builds#153372
stratakis wants to merge 2 commits into
python:mainfrom
stratakis:shared_probes

Conversation

@stratakis

@stratakis stratakis commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

When Python is configured with --with-dtrace and --enable-shared, its USDT probes are in libpython, not the executable. The SystemTap and BPFTrace backends in test_dtrace were searching the executable, causing the tests to fail or time out. This PR makes both backends target the object containing the probes. Static builds are unaffected.

On Fedora, this can be reproduced by running test_dtrace for BPFTrace as sudo and for SystemTap either as sudo or as a user in the stapusr and stapdev groups.

Also added a commit to truncate the output of the tools as it can be quite verbose when it errors out (you can inspect by running the same tests without sudo).

@stratakis

Copy link
Copy Markdown
Contributor Author

cc @vstinner

Generate SystemTap probe definitions targeting libpython for shared
builds and use centralized USDT probe object discovery for readelf
and BPFTrace.
@stratakis

Copy link
Copy Markdown
Contributor Author

Rebased to fix conflicts

@vstinner

Copy link
Copy Markdown
Member

Python configured with: ./configure --with-pydebug --with-dtrace --enable-shared.

Without the change, I get:

# LANG= LD_LIBRARY_PATH=$PWD ./python -m test -v test_dtrace 
== CPython 3.16.0a0 (heads/main:ed716551e13, Jul 13 2026, 07:59:05) [GCC 16.1.1 20260515 (Red Hat 16.1.1-2)]
== Linux-7.0.12-201.fc44.x86_64-x86_64-with-glibc2.43 little-endian
== Python build: debug shared dtrace
== cwd: /home/vstinner/python/main/build/test_python_worker_3188349æ
== CPU count: 10 (process) / 12 (system)
== encodings: locale=UTF-8 FS=utf-8
== resources: all test resources are disabled, use -u option to unskip tests

Using random seed: 3520453529
0:00:00 load avg: 2.02 mem: 22.5 MiB Run 1 test sequentially in a single process
0:00:00 load avg: 2.02 mem: 22.5 MiB [1/1] test_dtrace
setUpClass (test.test_dtrace.BPFTraceNormalTests) ... skipped 'bpftrace timed out during usability check'
setUpClass (test.test_dtrace.BPFTraceOptimizedTests) ... skipped 'bpftrace timed out during usability check'
test_filter_probe_rows_ignores_warnings (test.test_dtrace.BPFTraceOutputTests.test_filter_probe_rows_ignores_warnings) ... ok
readelf version: 2.46
test_check_probes (test.test_dtrace.CheckDtraceProbes.test_check_probes) ... ok
test_missing_probes (test.test_dtrace.CheckDtraceProbes.test_missing_probes) ... expected failure
skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q\nUsage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.DTraceOptimizedTests) ... skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q\nUsage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.SystemTapNormalTests) ... skipped 'stap(1) failed: parse error: expected literal string or number\n        saw: operator \'*\' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3\n     source:   *cert = user_string($arg4);\n               ^\n\n1 parse error.\nstap_69e06176e3befa287a4f6741352860fa_990_src.c:12:10: fatal error: runtime_defines.h: No such file or directory\n   12 | #include "runtime_defines.h"\n      |          ^~~~~~~~~~~~~~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/scripts/Makefile.build:289: stap_69e06176e3befa287a4f6741352860fa_990_src.o] Error 1\nmake[2]: *** Waiting for unfinished jobs....\nstap_symbols.c:3:10: fatal error: sym.h: No such file or directory\n    3 | #include <sym.h>\n      |          ^~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/scripts/Makefile.build:289: stap_symbols.o] Error 1\nmake[1]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/Makefile:2140: .] Error 2\nmake: *** [Makefile:248: __sub-make] Error 2\nWARNING: kbuild exited with status: 2\nPass 4: compilation failed.  [man error::pass4]\nKernel version 7.0.12 is outside tested range 3.10 ... 7.0'
setUpClass (test.test_dtrace.SystemTapOptimizedTests) ... skipped 'stap(1) failed: parse error: expected literal string or number\n        saw: operator \'*\' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3\n     source:   *cert = user_string($arg4);\n               ^\n\n1 parse error.\nstap_69e06176e3befa287a4f6741352860fa_990_src.c:12:10: fatal error: runtime_defines.h: No such file or directory\n   12 | #include "runtime_defines.h"\n      |          ^~~~~~~~~~~~~~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/scripts/Makefile.build:289: stap_69e06176e3befa287a4f6741352860fa_990_src.o] Error 1\nmake[2]: *** Waiting for unfinished jobs....\nstap_symbols.c:3:10: fatal error: sym.h: No such file or directory\n    3 | #include <sym.h>\n      |          ^~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/scripts/Makefile.build:289: stap_symbols.o] Error 1\nmake[1]: *** [/usr/src/kernels/7.0.12-201.fc44.x86_64/Makefile:2140: .] Error 2\nmake: *** [Makefile:248: __sub-make] Error 2\nWARNING: kbuild exited with status: 2\nPass 4: compilation failed.  [man error::pass4]\nKernel version 7.0.12 is outside tested range 3.10 ... 7.0'

----------------------------------------------------------------------
Ran 3 tests in 24.814s

OK (skipped=6, expected failures=1)
0:00:24 load avg: 1.75 mem: 22.9 MiB [1/1] test_dtrace passed

== Tests result: SUCCESS ==

1 test OK.

Total duration: 24.9 sec
Total tests: run=3 skipped=6
Total test files: run=1/1
Result: SUCCESS

@stratakis

Copy link
Copy Markdown
Contributor Author

The systemtap tests require the same kernel-devel package as the running kernel, if it's not there the test will be skipped.

@vstinner

vstinner commented Aug 6, 2026

Copy link
Copy Markdown
Member

I tested again the change. Python built with ./configure --with-pydebug --with-dtrace --enable-shared and test_dtrace runs as root.

Without the change:

setUpClass (test.test_dtrace.BPFTraceNormalTests) ... skipped 'bpftrace timed out during usability check'
setUpClass (test.test_dtrace.BPFTraceOptimizedTests) ... skipped 'bpftrace timed out during usability check'
test_filter_probe_rows_ignores_warnings (test.test_dtrace.BPFTraceOutputTests.test_filter_probe_rows_ignores_warnings) ... ok
readelf version: 2.46
test_check_probes (test.test_dtrace.CheckDtraceProbes.test_check_probes) ... ok
test_missing_probes (test.test_dtrace.CheckDtraceProbes.test_missing_probes) ... expected failure
skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q\nUsage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.DTraceOptimizedTests) ... skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q\nUsage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.SystemTapNormalTests) ... skipped 'stap(1) failed: parse error: expected literal string or number\n        saw: operator \'*\' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3\n     source:   *cert = user_string($arg4);\n               ^\n\n1 parse error.\nstap_ad00f0008fef1242c8a51840ffeb6b1e_983_src.c:12:10: fatal error: runtime_defines.h: No such file or directory\n   12 | #include "runtime_defines.h"\n      |          ^~~~~~~~~~~~~~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/scripts/Makefile.build:289: stap_ad00f0008fef1242c8a51840ffeb6b1e_983_src.o] Error 1\nmake[2]: *** Waiting for unfinished jobs....\nstap_symbols.c:3:10: fatal error: sym.h: No such file or directory\n    3 | #include <sym.h>\n      |          ^~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/scripts/Makefile.build:289: stap_symbols.o] Error 1\nmake[1]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/Makefile:2192: .] Error 2\nmake: *** [Makefile:248: __sub-make] Error 2\nWARNING: kbuild exited with status: 2\nPass 4: compilation failed.  [man error::pass4]\nKernel version 7.1.4 is outside tested range 3.10 ... 7.0'
setUpClass (test.test_dtrace.SystemTapOptimizedTests) ... skipped 'stap(1) failed: parse error: expected literal string or number\n        saw: operator \'*\' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3\n     source:   *cert = user_string($arg4);\n               ^\n\n1 parse error.\nstap_ad00f0008fef1242c8a51840ffeb6b1e_983_src.c:12:10: fatal error: runtime_defines.h: No such file or directory\n   12 | #include "runtime_defines.h"\n      |          ^~~~~~~~~~~~~~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/scripts/Makefile.build:289: stap_ad00f0008fef1242c8a51840ffeb6b1e_983_src.o] Error 1\nmake[2]: *** Waiting for unfinished jobs....\nstap_symbols.c:3:10: fatal error: sym.h: No such file or directory\n    3 | #include <sym.h>\n      |          ^~~~~~~\ncompilation terminated.\nmake[2]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/scripts/Makefile.build:289: stap_symbols.o] Error 1\nmake[1]: *** [/usr/src/kernels/7.1.4-204.fc44.x86_64/Makefile:2192: .] Error 2\nmake: *** [Makefile:248: __sub-make] Error 2\nWARNING: kbuild exited with status: 2\nPass 4: compilation failed.  [man error::pass4]\nKernel version 7.1.4 is outside tested range 3.10 ... 7.0'

----------------------------------------------------------------------
Ran 3 tests in 24.678s

OK (skipped=6, expected failures=1)
...
Total tests: run=3 skipped=6
Total test files: run=1/1

With the change:

test_function_entry_return (test.test_dtrace.BPFTraceNormalTests.test_function_entry_return) ... ok
test_gc (test.test_dtrace.BPFTraceNormalTests.test_gc) ... ok
test_verify_call_opcodes (test.test_dtrace.BPFTraceNormalTests.test_verify_call_opcodes)
Ensure our call stack test hits all function call opcodes ... ok
test_function_entry_return (test.test_dtrace.BPFTraceOptimizedTests.test_function_entry_return) ... ok
test_gc (test.test_dtrace.BPFTraceOptimizedTests.test_gc) ... ok
test_verify_call_opcodes (test.test_dtrace.BPFTraceOptimizedTests.test_verify_call_opcodes)
Ensure our call stack test hits all function call opcodes ... ok
test_filter_probe_rows_ignores_warnings (test.test_dtrace.BPFTraceOutputTests.test_filter_probe_rows_ignores_warnings) ... ok
readelf version: 2.46
test_check_probes (test.test_dtrace.CheckDtraceProbes.test_check_probes) ... ok
test_missing_probes (test.test_dtrace.CheckDtraceProbes.test_missing_probes) ... expected failure
skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q; Usage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.DTraceOptimizedTests) ... skipped 'dtrace(1) failed: /usr/sbin/dtrace invalid option -q; Usage /usr/sbin/dtrace [--help] [-h | -G] [-C [-I<Path>]] -s File.d [-o <File>]'
setUpClass (test.test_dtrace.SystemTapNormalTests) ... skipped "stap(1) failed: parse error: expected literal string or number; saw: operator '*' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3; source:   *cert = user_string($arg4);; ... (18 lines omitted)"
setUpClass (test.test_dtrace.SystemTapOptimizedTests) ... skipped "stap(1) failed: parse error: expected literal string or number; saw: operator '*' at /usr/share/systemtap/tapset/libvirt_probes-64.stp:204:3; source:   *cert = user_string($arg4);; ... (18 lines omitted)"

----------------------------------------------------------------------
Ran 9 tests in 7.410s

OK (skipped=4, expected failures=1)
...
Total tests: run=9 skipped=4
Total test files: run=1/1

So we get: "Total tests: run=3 skipped=6" => "Total tests: run=9 skipped=4". 6 more tests are run thanks to the change.

With the change, BPFTraceNormalTests and BPFTraceOptimizedTests tests are run.

Comment thread Lib/test/test_dtrace.py
PROBE_PLACEHOLDER = "@PYTHON_SYSTEMTAP_PROBE@"

@staticmethod
def _quote_systemtap_string(value):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to make the 3 added methods as private: you can remove the "_" prefix.

Comment thread Lib/test/test_dtrace.py
Comment on lines +231 to +236
executable = self._quote_systemtap_string(sys.executable)
probe_binary = get_probe_binary()
if probe_binary != sys.executable:
probe_binary = self._quote_systemtap_string(probe_binary)
return f'process("{executable}").library("{probe_binary}").mark'
return f'process("{executable}").mark'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
executable = self._quote_systemtap_string(sys.executable)
probe_binary = get_probe_binary()
if probe_binary != sys.executable:
probe_binary = self._quote_systemtap_string(probe_binary)
return f'process("{executable}").library("{probe_binary}").mark'
return f'process("{executable}").mark'
executable = self._quote_systemtap_string(sys.executable)
probe_binary = get_probe_binary()
if probe_binary == sys.executable:
return f'process("{executable}").mark'
# Python built with --enable-shared
probe_binary = self._quote_systemtap_string(probe_binary)
return f'process("{executable}").library("{probe_binary}").mark'

Comment thread Lib/test/test_dtrace.py
Comment on lines +238 to +242
def _render_script(self, script_file):
with open(script_file) as script:
return script.read().replace(
self.PROBE_PLACEHOLDER, self._python_probe()
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def _render_script(self, script_file):
with open(script_file) as script:
return script.read().replace(
self.PROBE_PLACEHOLDER, self._python_probe()
)
def _render_script(self, filename):
with open(filename) as fp:
script = fp.read()
return script.replace(self.PROBE_PLACEHOLDER, self._python_probe())

Comment thread Lib/test/test_dtrace.py
check_returncode=check_returncode,
)
finally:
os.unlink(generated_script_file)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add from test.support import os_helper at the top, and replace this line with:

Suggested change
os.unlink(generated_script_file)
os_helper.unlink(generated_script_file)

os_helper.unlink() tries harder to remove a file :-)

Comment thread Lib/test/test_dtrace.py
return binary


def truncate_output(output, *, max_lines=3, max_chars=500):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR only changes many functions. I would prefer to revert this truncate_output() change (remove the function, restore old code).

I'm not convinced that truncating the output is a good thing. On our CI, it's usually hard to reproduce locally a failure, and so we need to collect as much data as we want when an error occurs. I would prefer not truncating the output.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants